package com.hdu.distributeLock.lock;

import com.hdu.distributeLock.holder.ApplicationContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;


import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

import static java.util.Collections.singletonList;

/**
 * redis 分布式锁
 */

@Slf4j
public class DistributedRedisLock implements Lock {

    private final String LOCK_NAME;
    private final StringRedisTemplate STRING_REDIS_TEMPLATE;
    private final String IDENTITY;
    private static final Integer DEFAULT_EXPIRE_TIME = 30;
    private int expire;


    private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[1]) == 0 " +
            "or redis.call('hexists', KEYS[1], ARGV[1]) == 1\n" +
            "then \n" +
            "   redis.call('hincrby', KEYS[1], ARGV[1], 1)\n" +
            "   redis.call('expire', KEYS[1], ARGV[2])\n" +
            "   return 1\n" +
            "else \n" +
            "   return 0\n" +
            "end";


    private static final String UN_LOCK_SCRIPT = "if redis.call('hexists', KEYS[1], ARGV[1]) == 0\n" +
            "then \n" +
            "    return nil\n" +
            "elseif redis.call('hincrby', KEYS[1], ARGV[1], -1) == 0\n" +
            "then \n" +
            "    return redis.call('del', KEYS[1])\n" +
            "else \n" +
            "    return 0 \n" +
            "end";


    private static final String EXPIRE_SCRIPT = "if redis.call('hexists', KEYS[1], ARGV[1]) == 1\n" +
            "then \n" +
            "   return redis.call('expire', KEYS[1], ARGV[2])\n" +
            "else \n" +
            "   return 0\n" +
            "end";

    public DistributedRedisLock(String lockName, String applicationId) {
        this.LOCK_NAME = lockName;
        this.STRING_REDIS_TEMPLATE = ApplicationContextHolder.getBean(StringRedisTemplate.class);
        this.IDENTITY = applicationId + "-" + Thread.currentThread().getId();
    }


    @Override
    public void lock() {
        expire = DEFAULT_EXPIRE_TIME;
        Boolean result;
        do {
            result = executeLockScript();
            if (!result) {
                try {
                    TimeUnit.MILLISECONDS.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        } while (Boolean.FALSE.equals(result));

        if (Boolean.TRUE.equals(result)) {
            // 定期刷新
            renewExpire();
        }
    }

    @Override
    public void lockInterruptibly() {

    }

    @Override
    public boolean tryLock() {
        try {
            return tryLock(-1, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        expire = time != -1 ? (int) unit.toSeconds(time) : DEFAULT_EXPIRE_TIME;

        Boolean result = executeLockScript();

        if (Boolean.TRUE.equals(result)) {
            // 定期刷新
            renewExpire();
        }

        return Boolean.TRUE.equals(result);
    }



    @Override
    public void unlock() {
        Long result = this.STRING_REDIS_TEMPLATE.execute(
                new DefaultRedisScript<>(UN_LOCK_SCRIPT, Long.class),
                singletonList(LOCK_NAME),
                getIDENTITY()
        );
        if (Objects.isNull(result)) {
            throw new IllegalMonitorStateException("this lock does not belong to you");
        }
    }


    private void renewExpire() {
        new Timer().schedule(
                new TimerTask() {
                    @Override
                    public void run() {
                        Boolean result = executeExpireScript();
                        if (Boolean.TRUE.equals(result)) {
                            log.info("锁续期成功...");
                            renewExpire();
                        } else {
                            log.error("锁续期失败");
                        }
                    }
                },
                this.expire * 1000L / 3
        );
    }


    private boolean executeLockScript() {
        Boolean result = this.STRING_REDIS_TEMPLATE.execute(
                new DefaultRedisScript<>(LOCK_SCRIPT, Boolean.class),
                singletonList(LOCK_NAME),
                getIDENTITY(),
                String.valueOf(expire)
        );
        return Boolean.TRUE.equals(result);
    }

    protected boolean executeExpireScript() {
        Boolean result = STRING_REDIS_TEMPLATE.execute(
                new DefaultRedisScript<>(EXPIRE_SCRIPT, Boolean.class),
                singletonList(LOCK_NAME),
                getIDENTITY(),
                String.valueOf(expire)
        );
        return Boolean.TRUE.equals(result);
    }


    @Override
    public Condition newCondition() {
        return null;
    }

    private String getIDENTITY() {
        return IDENTITY;
    }
}
